/*
 *          Vanish Firefox Plugin MPL Version 1.1 License
 * 
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 * 
 * The Original Code is "Vanish Firefox Plugin"
 * 
 * The Initial Owner of the Original Code is the University of Washington.
 * The code is Copyright (C) 2009 by University of Washington. All
 * Rights Reserved.
 * 
 * Contributor(s): Amit Levy
 */

/*
   Class: XMLRpc
   XMLRpc javascript library
 */

function XMLRpc(xmlHttpRequest, server, method, params) {
	this.request = xmlHttpRequest;
	this.server = server;
	this.method = method;
	this.time = 0;
	if (params !== undefined) {
		this.params = params;
	} else {
		this.params = [];
	}
}

XMLRpc.prototype.call = function() {
	var time = new Date();
    this.request.open("POST", this.server, false);
    this.request.send(this.requestXml());
    this.time = new Date() - time;
    if (this.request.status == 200) {
    	var parser = new DOMParser();
    	return XMLRpc.parseResponse(parser.parseFromString(this.request.responseText,
		"text/xml"));
    } else {
    	return false;
    }
};

XMLRpc.prototype.callAsync = function(callback, callErr, callFinal) {
	var time = new Date();
    this.request.onreadystatechange = function() {
        if (this.readyState == 4) {
	        try {
              if (this.status == 200) {
            	  var parser = new DOMParser();
	              var result = XMLRpc.parseResponse(parser.parseFromString(this.responseText,
	        	  "text/xml"));
	               callback(result, new Date() - time);
              } else {
            	  throw "XMLRpc error: Request failed/timed out";
              }
	        } catch (e) {
	            if(callErr) {
	            	callErr(e);
	            }
	        } finally {
	          if (callFinal) {
	            callFinal();
	          }
	        }
        }
    };
    this.request.open("POST", this.server, true);
    this.request.send(this.requestXml());
    setTimeout(function(request) {
    		request.abort();
    }, 60000, this.request);
};

XMLRpc.prototype.requestXml = function() {
    var out = "<?xml version=\"1.0\"?>\n";
    out += "<methodCall>\n";
    out += "<methodName>"+ this.method + "</methodName>\n";

    if (this.params && this.params.length > 0) {
        out += "<params>\n";
        for (var i=0; i < this.params.length; i++) {
        	out += this.params[i].xml();
        }
        out += "</params>\n";
    }

    out += "</methodCall>\n";
    return out;
};

/**
 * XMLRpcParam
 * 
 * @param param
 * @return
 */
function XMLRpcParam(param) {
	this.param = param;
}

XMLRpcParam.prototype.xml = function() {
	function toXml(prm) {
		var value = "<nil />";
		if (prm !== null && prm !== undefined) {
			var out;
		    switch (typeof(prm)) {
		        case "boolean":
		        	value = "<boolean>" + prm + "</boolean>";
		        	break;
		        case "string":
		            prm = prm.replace(/</g, "&lt;");
		            prm = prm.replace(/&/g, "&amp;");
		            value = "<string>" + prm + "</string>";
		            break;
		        case "undefined":
		        	value = "<nil/>";
		        	break;
		        case "number":
		            if (/\./.test(prm)) {
		                value = "<double>" + prm + "</double>";
		            } else {
		                value = "<int>" + prm + "</int>";
		            }
		            break;
		        case "object":
		        	if (prm.constructor == VBase64) {
		        		value = "<base64>" + prm.data + "</base64>";
		        	} else if (prm.constructor == Array) {
		                out = "<array><data>\n";
		                for (var i in prm) {
		                    out += "  <value>";
		                    toXml(prm[i]);
		                    out += "</value>\n";
		                }
		                out += "</data></array>";
		                value = out;
		            } else if (prm.constructor == Date) {
		                out = "<dateTime.iso8601>";
		                out += prm.getUTCFullYear();
		                if (prm.getUTCMonth() < 10) {
		                    out += "0";
		                }
		                out += prm.getUTCMonth();
		                if (prm.getUTCDate() < 10) {
		                    out += "0";
		                }
		                out += prm.getUTCDate() + "T";
		                if (prm.getUTCHours() < 10) {
		                    out += "0";
		                }
		                out += prm.getUTCHours() + ":";
		                if (prm.getUTCMinutes() < 10) {
		                    out += "0";
		                }
		                out += prm.getUTCMinutes() + ":";
		                if (prm.getUTCSeconds() < 10) {
		                    out += "0";
		                }
		                out += prm.getUTCSeconds();
		                out += "</dateTime.iso8601>";
		                value = out;
		            } else { /* struct */
		                out = "<struct>\n";
		                for (var i in prm) {
		                    out += "<member>";
		                    out += "<name>" + i + "</name>";
		                    out += "<value>" + toXml(prm[i]) + "</value>";
		                    out += "</member>\n";
		                }
		                out += "</struct>\n";
		                value = out;
		            }
		            break;
		    }
		}
		return value;
	}
	return "<param><value>" + toXml(this.param) + "</value></param>";
};

XMLRpc.parseResponse = function (dom) {
    var methResp = dom.childNodes[dom.childNodes.length - 1];
    if (methResp.nodeName != "methodResponse") {
        throw "malformed <methodResponse>, got " + methResp.nodeName;
    }

    var params = methResp.childNodes[0];
    if (params.nodeName == "fault")  {
        var fault = parseXmlValue(params.childNodes[0]);
        throw fault.faultString;
    }
    if (params.nodeName != "params") {
        throw "malformed <params>, got <" + params.nodeName + ">";
    }
    var param = params.childNodes[0];
    if (param.nodeName != "param") {
        throw "malformed <param>, got <" + param.nodeName + ">";
    }
    var value = param.childNodes[0];
    if (value.nodeName != "value") {
        throw "malformed <value>, got <" + value.nodeName + ">";
    }
    return parseXmlValue(value);
};

function parseXmlValue(value) {
    if (value.nodeName != "value") {
        throw "parser: expected <value>";
    }

    var type = value.childNodes[0];
    if (type === null) {
        throw "parser: expected <value> to have a child";
    }
    switch (type.nodeName) {
        case "boolean":
            return type.childNodes[0].data == "1" ? true : false;
        case "i4":
        case "int":
            return parseInt(type.childNodes[0].data);
        case "double":
            return parseFloat(type.childNodes[0].data);
        case "#text": // Apache XML-RPC 2 doesn't wrap strings with <string>
            return type.data;
        case "string":
            return type.childNodes[0].data;
        case "array":
            var data = type.childNodes[0];
            var res = [];
            for (var i=0; i < data.childNodes.length; i++) {
                res[i] = parseXmlValue(data.childNodes[i]);
            }
            return res;
        case "struct":
            var members = type.childNodes;
            var res = {};
            for (var i=0; i < members.length; i++) {
                var name = members[i].childNodes[0].childNodes[0].data;
                var value = parseXmlValue(members[i].childNodes[1]);
                res[name] = value;
            }
            return res;
        case "dateTime.iso8601":
            var s = type.childNodes[0].data;
            var d = new Date();
            d.setUTCFullYear(s.substr(0, 4));
            d.setUTCMonth(parseInt(s.substr(4, 2)) - 1);
            d.setUTCDate(s.substr(6, 2));
            d.setUTCHours(s.substr(9, 2));
            d.setUTCMinutes(s.substr(12, 2));
            d.setUTCSeconds(s.substr(15, 2));
            return d;
        case "base64":
            return new VBase64(type.childNodes[0].data);
        default:
            throw "parser: expected type, got <"+type.nodeName+">";
    }
}

function VBase64(data) {
	this.data = data;
}

VBase64.prototype.toString = function() {
	return this.data;
};

VBase64.prototype.decode = function() {
	return b64.decode(this.data);
};

VBase64.encode = function(str) {
	return new VBase64(b64.encode(str));
};

/**
 * 
 * VBase64 encode / decode http://www.webtoolkit.info/
 * 
 */
var b64 = {
 
	// private property
	_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 
	// public method for encoding
	encode : function (input) {
		var output = "";
		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		var i = 0;
 
		input = b64._utf8_encode(input);
 
		while (i < input.length) {
 
			chr1 = input.charCodeAt(i++);
			chr2 = input.charCodeAt(i++);
			chr3 = input.charCodeAt(i++);
 
			enc1 = chr1 >> 2;
			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			enc4 = chr3 & 63;
 
			if (isNaN(chr2)) {
				enc3 = enc4 = 64;
			} else if (isNaN(chr3)) {
				enc4 = 64;
			}
 
			output = output +
			this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
			this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
 
		}
 
		return output;
	},
 
	// public method for decoding
	decode : function (input) {
		var output = "";
		var chr1, chr2, chr3;
		var enc1, enc2, enc3, enc4;
		var i = 0;
		input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
 
		while (i < input.length) {
 
			enc1 = this._keyStr.indexOf(input.charAt(i++));
			enc2 = this._keyStr.indexOf(input.charAt(i++));
			enc3 = this._keyStr.indexOf(input.charAt(i++));
			enc4 = this._keyStr.indexOf(input.charAt(i++));
 
			chr1 = (enc1 << 2) | (enc2 >> 4);
			chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
			chr3 = ((enc3 & 3) << 6) | enc4;
 
			output = output + String.fromCharCode(chr1);
 
			if (enc3 != 64) {
				output = output + String.fromCharCode(chr2);
			}
			if (enc4 != 64) {
				output = output + String.fromCharCode(chr3);
			}
 
		}
 
		output = b64._utf8_decode(output);
 
		return output;
 
	},
 
	// private method for UTF-8 encoding
	_utf8_encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
 
		for (var n = 0; n < string.length; n++) {
 
			var c = string.charCodeAt(n);
 
			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
 
		}
 
		return utftext;
	},
 
	// private method for UTF-8 decoding
	_utf8_decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
 
		while ( i < utftext.length ) {
 
			c = utftext.charCodeAt(i);
 
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
 
		}
 
		return string;
	}
 
}
